package ast

import (
	"go/ast"
	"go/doc"
	"go/parser"
	"go/token"
	"log"
	"os"
	"path/filepath"
)

//var msgRegexp = regexp.MustCompile(`\s*message(?:\s*=\s*(\d+))?\s*`)

type Package struct {
	FSet       *token.FileSet
	Dir        string
	Name       string
	ConstTypes map[string]string
	Files      map[string]*File
}

type File struct {
	Package    string
	FilePath   string
	ConstTypes map[string]string
	Structs    map[string]*Struct
}

type Struct struct {
	Package string
	Name    string
	Fields  []*Field
}

type Field struct {
	Name     string
	Type     *Type
	IsFixLen bool
}

type Type struct {
	Name      string
	DefName   string
	Size      int
	Len       string
	Type      *Type
	IsUnknown bool
	IsPoint   bool
	IsArray   bool
}

func (p *Package) File(pos token.Pos) *File {
	filename := p.FSet.File(pos).Name()
	file, exists := p.Files[filename]
	if !exists {
		file = &File{
			Package:    p.Name,
			ConstTypes: p.ConstTypes,
			Structs:    make(map[string]*Struct),
		}
		p.Files[filename] = file
	}
	return file
}

func AnalyzeDir(dir string, ignoreExtent []string) *Package {
	pkgName, fset, files := parseFiles(dir, ignoreExtent)
	pkgAst, _ := ast.NewPackage(fset, files, nil, nil)
	pkgDoc := doc.New(pkgAst, pkgName, doc.AllDecls)

	pkg := &Package{
		FSet:       fset,
		Dir:        dir,
		Name:       pkgName,
		ConstTypes: make(map[string]string),
		Files:      make(map[string]*File),
	}

	analyzeConstTypes(pkg, pkgDoc)

	analyzeStructs(pkg, pkgDoc)

	return pkg
}

func parseFiles(dir string, ignoreExtent []string) (string, *token.FileSet, map[string]*ast.File) {
	var pkgName string

	fset := token.NewFileSet()

	files := make(map[string]*ast.File)

	_ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if filepath.Dir(path) != dir {
			// 跳过子目录
			return nil
		}

		for _, ie := range ignoreExtent {
			if ie == "" {
				continue
			}
			matched, err := filepath.Match("*"+ie, info.Name())
			if err != nil {
				log.Fatalf("match *%s file failed: %s", ie, err)
			}
			if matched {
				return nil
			}
		}

		isGoFile, err := filepath.Match("*.go", info.Name())
		if err != nil {
			log.Fatalf("match *.go file failed: %s", err)
		}
		if !isGoFile {
			return nil
		}

		file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
		if err != nil {
			log.Fatalf("Could't parse file '%s': %s", path, err)
		}

		_, fileName := filepath.Split(path)
		files[fileName] = file
		pkgName = file.Name.String()

		return nil
	})

	return pkgName, fset, files
}

func analyzeConstTypes(pkg *Package, pkgDoc *doc.Package) {
	for _, t := range pkgDoc.Types {
		if len(t.Consts) == 0 {
			continue
		}
		if typeSpec, ok := t.Decl.Specs[0].(*ast.TypeSpec); ok {
			declName := typeSpec.Name.String()
			typeName := typeSpec.Type.(*ast.Ident).Name
			pkg.ConstTypes[declName] = typeName
			//log.Printf("\tType '%s' -> '%s'", declName, typeName)
		}
	}
}

func analyzeStructs(pkg *Package, pkgDoc *doc.Package) {
	for _, t := range pkgDoc.Types {
		typeSpec, ok := t.Decl.Specs[0].(*ast.TypeSpec)
		if !ok {
			continue
		}

		structType, ok := typeSpec.Type.(*ast.StructType)
		if !ok {
			continue
		}

		s := analyzeStruct(pkg, t.Name, structType)
		file := pkg.File(t.Decl.Pos())
		file.Structs[t.Name] = s
	}
}

func analyzeStruct(pkg *Package, structName string, structType *ast.StructType) *Struct {
	si := &Struct{Name: structName}
	for _, field := range structType.Fields.List {
		typeInfo := analyzeType(pkg, field.Type)
		isFixLen := analyzeFixLen(typeInfo)
		for _, name := range field.Names {
			si.Fields = append(si.Fields, &Field{
				Name:     name.String(),
				Type:     typeInfo,
				IsFixLen: isFixLen,
			})
		}
	}
	return si
}

func analyzeFixLen(t *Type) bool {
	if t.IsArray {
		if t.Len == "" {
			return false
		}
		return analyzeFixLen(t.Type)
	}
	return t.Size != 0
}

func analyzeType(pkg *Package, astType ast.Expr) *Type {
	var ti Type
	switch t := astType.(type) {
	case *ast.StarExpr:
		ti.IsPoint = true
		ti.Type = analyzeType(pkg, t.X)
	case *ast.ArrayType:
		if size, ok := t.Len.(*ast.BasicLit); ok {
			ti.Len = size.Value
		}
		if t, ok := t.Elt.(*ast.Ident); ok {
			realName := t.Name
			if realName2, ok := pkg.ConstTypes[t.Name]; ok {
				realName = realName2
				if realName == "byte" {
					ti.DefName = t.Name
				}
			}
			if realName == "byte" {
				ti.Name = "[]byte"
				break
			}
		}
		ti.IsArray = true
		ti.Type = analyzeType(pkg, t.Elt)
	case *ast.Ident:
		ti.Name = t.Name
		ti.DefName = ti.Name
		if realName, ok := pkg.ConstTypes[t.Name]; ok {
			ti.DefName = ti.Name
			ti.Name = realName
		}
		switch ti.Name {
		case "string":
			ti.Size = 0
		case "int8", "uint8", "byte", "bool":
			ti.Size = 1
		case "int16", "uint16":
			ti.Size = 2
		case "int32", "uint32", "float32":
			ti.Size = 4
		case "int", "uint", "int64", "uint64", "float64":
			ti.Size = 8
		default:
			ti.IsUnknown = true
		}
	case *ast.SelectorExpr:
	default:
		log.Fatalf("Unsupported field type %#v", astType)
	}
	return &ti
}
